﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace DataExtractionLanguage
{
    public class Lexical : ILexical
    {
        private const string patternNumber = "^[0-9]+";
        private const string patternQuotedString = "^'[^'\r\n]*'";
        private const string patternDoublequotedString = "^\"[^\"\r\n]*\"";
        private const string patternIdentifier = "^[_A-Za-z][_A-Za-z0-9]*";
        private static Dictionary<string, TokenTypeEnum> keywords;

        static Lexical()
        {
            keywords = new Dictionary<string, TokenTypeEnum>();

            // Keywords
            keywords.Add("select", TokenTypeEnum.Select);
            keywords.Add("from", TokenTypeEnum.From);
            keywords.Add("where", TokenTypeEnum.Where);
            keywords.Add("as", TokenTypeEnum.As);
            keywords.Add("union", TokenTypeEnum.Union);
            keywords.Add("on", TokenTypeEnum.On);
            keywords.Add("inner", TokenTypeEnum.Inner);
            keywords.Add("join", TokenTypeEnum.Join);
            keywords.Add("outer", TokenTypeEnum.Outer);
            keywords.Add("left", TokenTypeEnum.Left);
            keywords.Add("right", TokenTypeEnum.Right);

            // Operators
            keywords.Add("and", TokenTypeEnum.OpAnd);
            keywords.Add("or", TokenTypeEnum.OpOr);
            keywords.Add("not", TokenTypeEnum.OpNot);

            // Properties
            keywords.Add("text", TokenTypeEnum.Text);
            keywords.Add("getattribute", TokenTypeEnum.GetAttribute);
            keywords.Add("countof", TokenTypeEnum.CountOf);
            keywords.Add("childcount", TokenTypeEnum.ChildCount);

            // Functions
            keywords.Add("substring", TokenTypeEnum.SubString);
            keywords.Add("length", TokenTypeEnum.Length);
            keywords.Add("like", TokenTypeEnum.Like);
            keywords.Add("indexof", TokenTypeEnum.IndexOf);
            keywords.Add("trim", TokenTypeEnum.Trim);
            keywords.Add("match", TokenTypeEnum.Match);
        }

        public Lexical(string source)
        {
            this.source = source;
            Location = new Location();
        }

        /// <summary>
        /// Source code to scan.
        /// </summary>
        private readonly string source;

        /// <summary>
        /// Strips white space.
        /// </summary>
        private void StripWhiteSpace()
        {
            char next = source[Location.Index];

            while (next == ' ' || next == '\t' || next == '\r' || next == '\n')
            {
                if (next == '\n')
                {
                    Location.Row++;
                    Location.Col = 1;
                }
                else if (next != '\r')
                    Location.Col++;

                Location.Index++;

                if (Location.Index >= source.Length)
                    break;

                next = source[Location.Index];
            }
        }

        /// <summary>
        /// Test if next token is an operator.
        /// </summary>
        /// <returns>True, If next token is an operator. False, otherwise.</returns>
        private bool IsOperator()
        {
            char next = source[Location.Index];
            int nextNextIndex = Location.Index + 1;

            Value = next;
            token = TokenTypeEnum.Error;
            switch (next)
            {
                case '!':
                    if (nextNextIndex < source.Length && source[nextNextIndex] == '=')
                    {
                        token = TokenTypeEnum.OpNotEqual;
                        Value = "!=";
                        Location.Index++;
                        Location.Col++;
                    }
                    break;

                case '=':
                    if (nextNextIndex < source.Length && source[nextNextIndex] == '=')
                    {
                        token = TokenTypeEnum.OpEqualEqual;
                        Value = "==";
                        Location.Index++;
                        Location.Col++;
                    }
                    break;

                case '>':
                    if (nextNextIndex < source.Length && source[nextNextIndex] == '=')
                    {
                        token = TokenTypeEnum.OpGreaterOrEqual;
                        Value = ">=";
                        Location.Index++;
                        Location.Col++;
                    }
                    else
                        token = TokenTypeEnum.OpGreater;
                    break;

                case '<':
                    if (nextNextIndex < source.Length && source[nextNextIndex] == '=')
                    {
                        token = TokenTypeEnum.OpLessOrEqual;
                        Value = "<=";
                        Location.Index++;
                        Location.Col++;
                    }
                    else
                        token = TokenTypeEnum.OpLess;
                    break;

                case '+':
                    token = TokenTypeEnum.OpPlus;
                    break;

                case '-':
                    token = TokenTypeEnum.OpMinus;
                    break;

                case '/':
                    token = TokenTypeEnum.OpDivide;
                    break;

                case '*':
                    token = TokenTypeEnum.OpMultiple;
                    break;

                default:
                    return false;
            }

            // An operator was found (warning: this test is need, see case of '!' without '=')
            if (token != TokenTypeEnum.Error)
            {
                Location.Index++;
                Location.Col++;
                return true;
            }

            return false;
        }

        /// <summary>
        /// Test if next token is a punctuation character.
        /// </summary>
        /// <returns>True, If next token is a punctuation character. False, otherwise.</returns>
        private bool IsPunctuation()
        {
            // Get next char
            char next = source[Location.Index];
            int nextNextIndex = Location.Index + 1;
            token = TokenTypeEnum.Error;

            // Compare to known operator
            switch (next)
            {
                case '{':
                    token = TokenTypeEnum.OpenParentheses;
                    break;

                case '}':
                    token = TokenTypeEnum.CloseParenthese;
                    break;

                case '[':
                    token = TokenTypeEnum.OpenBarBrackets;
                    break;

                case ']':
                    token = TokenTypeEnum.CloseBarBrackets;
                    break;

                case '(':
                    token = TokenTypeEnum.OpenBrackets;
                    break;

                case ')':
                    token = TokenTypeEnum.CloseBrackets;
                    break;

                case ';':
                    token = TokenTypeEnum.Semicolon;
                    break;

                case ',':
                    token = TokenTypeEnum.Comma;
                    break;

                case '.':
                    token = TokenTypeEnum.Dot;
                    break;

                default:
                    return false;
            }

            // Go to next char
            Value = next;
            Location.Index++;
            Location.Col++;

            return true;
        }

        /// <summary>
        /// Test if next token is a number.
        /// </summary>
        /// <returns>True, If next token is a number. False, otherwise.</returns>
        private bool IsNumber()
        {
            // Test if match with number pattern
            Match match = Regex.Match(source.Substring(Location.Index), patternNumber);
            if (!match.Success)
                return false;

            // Convert to int
            int length = match.Length;
            string number = source.Substring(Location.Index, length);
            Value = Convert.ToInt32(number);
            token = TokenTypeEnum.Number;

            Location.Index += length;
            Location.Col += length;

            return true;
        }

        /// <summary>
        /// Test if next token is a string.
        /// </summary>
        /// <returns>True, If next token is a string. False, otherwise.</returns>
        private bool IsString()
        {
            // Test first if match to pattern with single quote.
            string toTest = source.Substring(Location.Index);
            Match match = Regex.Match(toTest, patternQuotedString);

            // Then test with pattern with double quote
            if (!match.Success)
            {
                match = Regex.Match(toTest, patternDoublequotedString);
                if (!match.Success)
                    return false;
            }

            // Extract the value without quotes
            int length = match.Length;
            string stringValue = source.Substring(Location.Index + 1, length - 2);
            Value = stringValue;
            token = TokenTypeEnum.String;

            Location.Index += length;
            Location.Col += length;

            return true;
        }

        /// <summary>
        /// Test if next token is an Identifier or a keyword.
        /// </summary>
        /// <returns>True, If next token is an Identifier or keyword. False, otherwise.</returns>
        private bool IsIdentifierOrKeyword()
        {
            // Test if match with identifier pattern.
            Match match = Regex.Match(source.Substring(Location.Index), patternIdentifier);
            if (!match.Success)
                return false;

            // Extract the string
            int length = match.Length;
            string name = source.Substring(Location.Index, length);
            Location.Index += length;
            Location.Col += length;

            // If it's a keyword.
            if (keywords.ContainsKey(name.ToLower()))
            {
                token = keywords[name.ToLower()];
                Value = name;
                return true;
            }

            // It's just an Identifier
            token = TokenTypeEnum.Identifier;
            Value = name;
            return true;
        }

        #region ILexical Members

        /// <summary>
        /// Test if another token is available.
        /// </summary>
        /// <returns>True, if another token is available. False, otherwise.</returns>
        public bool Advance
        {
            get { return !string.IsNullOrEmpty(source) && Location.Index != -1 && Location.Index < source.Length; }
        }

        private TokenTypeEnum token;
        public TokenTypeEnum Token()
        {
            // After EOF
            if (Location.Index == -1)
                return TokenTypeEnum.Error;

            // Check for EOF
            if (!Advance)
            {
                Location.Index = -1;
                return token = TokenTypeEnum.EOF;
            }

            // Strip white space.
            StripWhiteSpace();

            // Check for EOF
            if (!Advance)
            {
                Location.Index = -1;
                return token = TokenTypeEnum.EOF;
            }

            // Look for all sort of token
            if (IsOperator())
                return token;
            else if (IsPunctuation())
                return token;
            else if (IsNumber())
                return token;
            else if (IsString())
                return token;
            else if (IsIdentifierOrKeyword())
                return token;

            // Unknown
            Value = source[Location.Index];
            return TokenTypeEnum.Error;
        }

        public object Value { get; set; }

        public TokenTypeEnum TokenType
        {
            get { return token; }
        }

        /// <summary>Gets current location of the scanning source.</summary>
        public Location Location { get; private set; }
        #endregion
    }
}
